Add omniauth to let huginn consume oauth endpoints

Created a service model which stores external account information.
Created a service controller which kicks off the oauth process and lets
the user manage their external accounts.
Oauthable concern connects a agent to a service and adds a drop down to the
agents edit page.
Added a migration which converts the existing twitter agents to use the
new service model.

Dominik Sander 10 jaren geleden
bovenliggende
commit
12c2af26bb

+ 12 - 0
.env.example

@@ -71,6 +71,18 @@ EMAIL_FROM_ADDRESS=from_address@gmail.com
71 71
 AGENT_LOG_LENGTH=200
72 72
 
73 73
 #############################
74
+#    OAuth Configuration    #
75
+#############################
76
+TWITTER_OAUTH_KEY=
77
+TWITTER_OAUTH_SECRET=
78
+
79
+37SIGNALS_OAUTH_KEY=
80
+37SIGNALS_OAUTH_SECRET=
81
+
82
+GITHUB_OAUTH_KEY=
83
+GITHUB_OAUTH_SECRET=
84
+
85
+#############################
74 86
 #  AWS and Mechanical Turk  #
75 87
 #############################
76 88
 

+ 1 - 1
.travis.yml

@@ -1,7 +1,7 @@
1 1
 language: ruby
2 2
 bundler_args: --without development production
3 3
 env:
4
-  - APP_SECRET_TOKEN=b2724973fd81c2f4ac0f92ac48eb3f0152c4a11824c122bcf783419a4c51d8b9bba81c8ba6a66c7de599677c7f486242cf819775c433908e77c739c5c8ae118d
4
+  - APP_SECRET_TOKEN=b2724973fd81c2f4ac0f92ac48eb3f0152c4a11824c122bcf783419a4c51d8b9bba81c8ba6a66c7de599677c7f486242cf819775c433908e77c739c5c8ae118d TWITTER_OAUTH_KEY=twitteroauthkey TWITTER_OAUTH_SECRET=twitteroauthsecret
5 5
 rvm:
6 6
   - 2.0.0
7 7
   - 2.1.1

+ 5 - 0
Gemfile

@@ -76,6 +76,11 @@ gem 'therubyracer', '~> 0.12.1'
76 76
 
77 77
 gem 'mqtt'
78 78
 
79
+gem 'omniauth'
80
+gem 'omniauth-twitter'
81
+gem 'omniauth-37signals'
82
+gem 'omniauth-github'
83
+
79 84
 group :development do
80 85
   gem 'binding_of_caller'
81 86
   gem 'better_errors'

+ 25 - 0
Gemfile.lock

@@ -168,12 +168,33 @@ GEM
168 168
     naught (1.0.0)
169 169
     nokogiri (1.6.2.1)
170 170
       mini_portile (= 0.6.0)
171
+    oauth (0.4.7)
171 172
     oauth2 (0.9.3)
172 173
       faraday (>= 0.8, < 0.10)
173 174
       jwt (~> 0.1.8)
174 175
       multi_json (~> 1.3)
175 176
       multi_xml (~> 0.5)
176 177
       rack (~> 1.2)
178
+    omniauth (1.2.1)
179
+      hashie (>= 1.2, < 3)
180
+      rack (~> 1.0)
181
+    omniauth-37signals (1.0.5)
182
+      omniauth (~> 1.0)
183
+      omniauth-oauth2 (~> 1.0)
184
+    omniauth-github (1.1.2)
185
+      omniauth (~> 1.0)
186
+      omniauth-oauth2 (~> 1.1)
187
+    omniauth-oauth (1.0.1)
188
+      oauth
189
+      omniauth (~> 1.0)
190
+    omniauth-oauth2 (1.1.2)
191
+      faraday (>= 0.8, < 0.10)
192
+      multi_json (~> 1.3)
193
+      oauth2 (~> 0.9.3)
194
+      omniauth (~> 1.2)
195
+    omniauth-twitter (1.0.1)
196
+      multi_json (~> 1.3)
197
+      omniauth-oauth (~> 1.0)
177 198
     orm_adapter (0.5.0)
178 199
     polyglot (0.3.5)
179 200
     protected_attributes (1.0.7)
@@ -345,6 +366,10 @@ DEPENDENCIES
345 366
   mqtt
346 367
   mysql2 (~> 0.3.15)
347 368
   nokogiri (~> 1.6.1)
369
+  omniauth
370
+  omniauth-37signals
371
+  omniauth-github
372
+  omniauth-twitter
348 373
   protected_attributes (~> 1.0.7)
349 374
   pry
350 375
   rack

+ 32 - 0
app/concerns/oauthable.rb

@@ -0,0 +1,32 @@
1
+module Oauthable
2
+  extend ActiveSupport::Concern
3
+
4
+  included do |base|
5
+    attr_accessible :service_id
6
+    validates_presence_of :service_id
7
+    base.extend ClassMethods
8
+    self.class_variable_set(:@@valid_oauth_providers, :all)
9
+  end
10
+
11
+  def oauthable?
12
+    true
13
+  end
14
+
15
+  def valid_services(current_user)
16
+    if valid_oauth_providers == :all
17
+      current_user.available_services
18
+    else
19
+      current_user.available_services.where(provider: valid_oauth_providers)
20
+    end
21
+  end
22
+
23
+  def valid_oauth_providers
24
+    self.class.class_variable_get(:@@valid_oauth_providers)
25
+  end
26
+
27
+  module ClassMethods
28
+    def valid_oauth_providers(*providers)
29
+      self.class_variable_set(:@@valid_oauth_providers, providers)
30
+    end
31
+  end
32
+end

+ 6 - 4
app/concerns/twitter_concern.rb

@@ -1,8 +1,10 @@
1 1
 module TwitterConcern
2 2
   extend ActiveSupport::Concern
3
+  include Oauthable
3 4
 
4 5
   included do
5 6
     validate :validate_twitter_options
7
+    valid_oauth_providers :twitter
6 8
   end
7 9
 
8 10
   def validate_twitter_options
@@ -15,19 +17,19 @@ module TwitterConcern
15 17
   end
16 18
 
17 19
   def twitter_consumer_key
18
-    options['consumer_key'].presence || credential('twitter_consumer_key')
20
+    ENV['TWITTER_OAUTH_KEY']
19 21
   end
20 22
 
21 23
   def twitter_consumer_secret
22
-    options['consumer_secret'].presence || credential('twitter_consumer_secret')
24
+    ENV['TWITTER_OAUTH_SECRET']
23 25
   end
24 26
 
25 27
   def twitter_oauth_token
26
-    options['oauth_token'].presence || options['access_key'].presence || credential('twitter_oauth_token')
28
+    self.service.token
27 29
   end
28 30
 
29 31
   def twitter_oauth_token_secret
30
-    options['oauth_token_secret'].presence || options['access_secret'].presence || credential('twitter_oauth_token_secret')
32
+    self.service.secret
31 33
   end
32 34
 
33 35
   def twitter

+ 40 - 0
app/controllers/services_controller.rb

@@ -0,0 +1,40 @@
1
+class ServicesController < ApplicationController
2
+
3
+  def index
4
+    @services = current_user.services.page(params[:page])
5
+
6
+    respond_to do |format|
7
+      format.html
8
+      format.json { render json: @services }
9
+    end
10
+  end
11
+
12
+  def destroy
13
+    @services = current_user.services.find(params[:id])
14
+    @services.destroy
15
+
16
+    respond_to do |format|
17
+      format.html { redirect_to services_path }
18
+      format.json { head :no_content }
19
+    end
20
+  end
21
+
22
+  def toggle_availability
23
+    @service = current_user.services.find(params[:id])
24
+    @service.toggle_availability!
25
+
26
+    respond_to do |format|
27
+      format.html { redirect_to services_path }
28
+      format.json { render json: @service }
29
+    end
30
+  end
31
+
32
+  def callback
33
+    @service = current_user.services.initialize_or_update_via_omniauth(request.env['omniauth.auth'])
34
+    if @service && @service.save
35
+      redirect_to services_path, notice: "The service was successfully created."
36
+    else
37
+      redirect_to services_path, error: "Error creating the service."
38
+    end
39
+  end
40
+end

+ 1 - 0
app/models/agent.rb

@@ -40,6 +40,7 @@ class Agent < ActiveRecord::Base
40 40
   after_save :possibly_update_event_expirations
41 41
 
42 42
   belongs_to :user, :inverse_of => :agents
43
+  belongs_to :service
43 44
   has_many :events, -> { order("events.id desc") }, :dependent => :delete_all, :inverse_of => :agent
44 45
   has_one  :most_recent_event, :inverse_of => :agent, :class_name => "Event", :order => "events.id desc"
45 46
   has_many :logs,  -> { order("agent_logs.id desc") }, :dependent => :delete_all, :inverse_of => :agent, :class_name => "AgentLog"

+ 8 - 14
app/models/agents/basecamp_agent.rb

@@ -2,17 +2,16 @@ module Agents
2 2
   class BasecampAgent < Agent
3 3
     cannot_receive_events!
4 4
 
5
+    include Oauthable
6
+    valid_oauth_providers '37signals'
7
+
5 8
     description <<-MD
6 9
       The BasecampAgent checks a Basecamp project for new Events
7 10
 
8
-      It is required that you enter your Basecamp credentials (`username` and `password`).
9
-
10
-      You also need to provide your Basecamp `user_id` and the `project_id` of the project you want to monitor.
11
+      You need to provide the `project_id` of the project you want to monitor.
11 12
       If you have your Basecamp project opened in your browser you can find the user_id and project_id as follows:
12 13
 
13
-      `https://basecamp.com/`
14
-      user_id
15
-      `/projects/`
14
+      `https://basecamp.com/123456/projects/`
16 15
       project_id
17 16
       `-explore-basecamp`
18 17
     MD
@@ -45,17 +44,11 @@ module Agents
45 44
 
46 45
     def default_options
47 46
       {
48
-        'username' => '',
49
-        'password' => '',
50
-        'user_id' => '',
51 47
         'project_id' => '',
52 48
       }
53 49
     end
54 50
 
55 51
     def validate_options
56
-      errors.add(:base, "you need to specify your basecamp username") unless options['username'].present?
57
-      errors.add(:base, "you need to specify your basecamp password") unless options['password'].present?
58
-      errors.add(:base, "you need to specify your basecamp user id") unless options['user_id'].present?
59 52
       errors.add(:base, "you need to specify the basecamp project id of which you want to receive events") unless options['project_id'].present?
60 53
     end
61 54
 
@@ -64,6 +57,7 @@ module Agents
64 57
     end
65 58
 
66 59
     def check
60
+      self.service.prepare_request
67 61
       reponse = HTTParty.get request_url, request_options.merge(query_parameters)
68 62
       memory[:last_run] = Time.now.utc.iso8601
69 63
       if last_check_at != nil
@@ -76,11 +70,11 @@ module Agents
76 70
 
77 71
   private
78 72
     def request_url
79
-      "https://basecamp.com/#{URI.encode(options[:user_id].to_s)}/api/v1/projects/#{URI.encode(options[:project_id].to_s)}/events.json"
73
+      "https://basecamp.com/#{URI.encode(self.service.options[:user_id].to_s)}/api/v1/projects/#{URI.encode(options[:project_id].to_s)}/events.json"
80 74
     end
81 75
 
82 76
     def request_options
83
-      {:basic_auth => {:username =>options[:username], :password=>options[:password]}, :headers => {"User-Agent" => "Huginn (https://github.com/cantino/huginn)"}}
77
+      {:headers => {"User-Agent" => "Huginn (https://github.com/cantino/huginn)", "Authorization" => "Bearer \"#{self.service.token}\""}}
84 78
     end
85 79
 
86 80
     def query_parameters

+ 59 - 0
app/models/service.rb

@@ -0,0 +1,59 @@
1
+class Service < ActiveRecord::Base
2
+  attr_accessible :provider, :name, :token, :secret, :refresh_token, :expires_at, :global, :options
3
+
4
+  serialize :options, Hash
5
+
6
+  belongs_to :user
7
+
8
+  validates_presence_of :user_id, :provider, :name, :token
9
+
10
+  def toggle_availability!
11
+    self.global = !self.global
12
+    self.save!
13
+  end
14
+
15
+  def prepare_request
16
+    if self.expires_at && Time.now > self.expires_at
17
+      self.refresh_token!
18
+    end
19
+  end
20
+
21
+  def refresh_token!
22
+    response = HTTParty.post(endpoint, query: {
23
+                  type:          'refresh',
24
+                  client_id:     ENV["#{self.provider.upcase}_OAUTH_KEY"],
25
+                  client_secret: ENV["#{self.provider.upcase}_OAUTH_SECRET"],
26
+                  refresh_token: self.refresh_token
27
+    })
28
+    data = JSON.parse(response.body)
29
+    self.update(expires_at: Time.now + data['expires_in'], token: data['access_token'], refresh_token: data['refresh_token'].presence || self.refresh_token)
30
+  end
31
+
32
+  def self.initialize_or_update_via_omniauth(omniauth)
33
+    case omniauth['provider']
34
+    when 'twitter'
35
+      find_or_initialize_by(provider: omniauth['provider'], name: omniauth['info']['nickname']).tap do |service|
36
+        service.assign_attributes(token: omniauth['credentials']['token'], secret: omniauth['credentials']['secret'])
37
+      end
38
+    when 'github'
39
+      find_or_initialize_by(provider: omniauth['provider'], name: omniauth['info']['nickname']).tap do |service|
40
+        service.assign_attributes(token: omniauth['credentials']['token'])
41
+      end
42
+    when '37signals'
43
+      find_or_initialize_by(provider: omniauth['provider'], name: omniauth['info']['name']).tap do |service|
44
+        service.assign_attributes(token: omniauth['credentials']['token'],
45
+                                  refresh_token: omniauth['credentials']['refresh_token'],
46
+                                  expires_at: Time.at(omniauth['credentials']['expires_at']),
47
+                                  options: {user_id: omniauth['extra']['accounts'][0]['id']})
48
+      end
49
+    else
50
+      false
51
+    end
52
+  end
53
+
54
+  private
55
+  def endpoint
56
+    client_options = "OmniAuth::Strategies::#{OmniAuth::Utils.camelize(self.provider)}".constantize.default_options['client_options']
57
+    URI.join(client_options['site'], client_options['token_url'])
58
+  end
59
+end

+ 6 - 0
app/models/user.rb

@@ -26,6 +26,12 @@ class User < ActiveRecord::Base
26 26
   has_many :events, -> { order("events.created_at desc") }, :dependent => :delete_all, :inverse_of => :user
27 27
   has_many :agents, -> { order("agents.created_at desc") }, :dependent => :destroy, :inverse_of => :user
28 28
   has_many :logs, :through => :agents, :class_name => "AgentLog"
29
+  has_many :services, -> { order("services.name")}, :dependent => :destroy
30
+  
31
+
32
+  def available_services
33
+    Service.where("user_id = #{self.id} or global = true").order("services.name desc") 
34
+  end
29 35
 
30 36
   # Allow users to login via either email or username.
31 37
   def self.find_first_by_auth_conditions(warden_conditions)

+ 8 - 1
app/views/agents/_form.html.erb

@@ -25,11 +25,18 @@
25 25
             </div>
26 26
           <% end %>
27 27
 
28
-          <div class="form-group">
28
+          <div class="form-group type-select">
29 29
             <%= f.label :name %>
30 30
             <%= f.text_field :name, :class => 'form-control' %>
31 31
           </div>
32 32
 
33
+          <% if @agent.try(:oauthable?) %>
34
+            <div class="form-group type-select">
35
+              <%= f.label :service %>
36
+              <%= f.select :service_id, options_for_select(@agent.valid_services(current_user).collect { |s| ["(#{s.provider}) #{s.name}", s.id]}, @agent.service_id),{}, class: 'form-control' %>
37
+            </div>
38
+          <% end %>
39
+
33 40
           <div class="form-group">
34 41
             <%= f.label :schedule, :class => 'control-label' %>
35 42
             <div class="schedule-region" data-can-be-scheduled="<%= @agent.can_be_scheduled? %>">

+ 1 - 0
app/views/layouts/_navigation.html.erb

@@ -15,6 +15,7 @@
15 15
       <%= nav_link "Agents", agents_path %>
16 16
       <%= nav_link "Events", events_path %>
17 17
       <%= nav_link "Credentials", user_credentials_path %>
18
+      <%= nav_link "Services", services_path %>
18 19
     </ul>
19 20
   <% end %>
20 21
   

+ 52 - 0
app/views/services/index.html.erb

@@ -0,0 +1,52 @@
1
+<div class='container'>
2
+  <div class='row'>
3
+    <div class='col-md-12'>
4
+      <div class="page-header">
5
+        <h2>
6
+          Your Services
7
+        </h2>
8
+      </div>
9
+      <p>
10
+        Before you can authenticate with a service, you need to set it up. Have a look at the
11
+        <%= link_to 'wiki', 'tobedone', target: :_blank %>
12
+        for guidance.
13
+      </p>
14
+      <p><%= link_to "Authenticate with Twitter", "/auth/twitter" %></p>
15
+      <p><%= link_to "Authenticate with 37Signals (Basecamp)", "/auth/37signals" %></p>
16
+      <p><%= link_to "Authenticate with Github", "/auth/github" %></p>
17
+      <hr>
18
+
19
+      <div class='table-responsive'>
20
+        <table class='table table-striped events'>
21
+          <tr>
22
+            <th>Provider</th>
23
+            <th>Username</th>
24
+            <th>Global?</th>
25
+            <th></th>
26
+          </tr>
27
+
28
+        <% @services.each do |service| %>
29
+          <tr>
30
+            <td><%= service.provider %></td>
31
+            <td><%= service.name %></td>
32
+            <td><%= service.global ? 'Yes' : 'No' %></td>
33
+            <td>
34
+              <div class="btn-group btn-group-xs">
35
+                <% if service.global %>
36
+                  <%= link_to 'Make private', toggle_availability_service_path(service), method: :post, data: { confirm: 'Are you sure you want to remove the access to this service for every user?'}, class: "btn btn-default" %>
37
+                <% else %>
38
+                   <%= link_to 'Make global', toggle_availability_service_path(service), method: :post, data: { confirm: 'Are you sure you want to grant every user access to this service?'}, class: "btn btn-default" %>
39
+                <% end %>
40
+                <%= link_to 'Delete', service_path(service), method: :delete, data: { confirm: 'Are you sure?' }, class: "btn btn-default btn-danger" %>
41
+              </div>
42
+            </td>
43
+          </tr>
44
+        <% end %>
45
+        </table>
46
+      </div>
47
+
48
+      <%= paginate @services, :theme => 'twitter-bootstrap-3' %>
49
+    </div>
50
+  </div>
51
+</div>
52
+

+ 5 - 0
config/initializers/omniauth.rb

@@ -0,0 +1,5 @@
1
+Rails.application.config.middleware.use OmniAuth::Builder do
2
+  provider :twitter, ENV['TWITTER_OAUTH_KEY'], ENV['TWITTER_OAUTH_SECRET'], authorize_params: {force_login: 'true', use_authorize: 'true'}
3
+  provider '37signals', ENV['37SIGNALS_OAUTH_KEY'], ENV['37SIGNALS_OAUTH_SECRET']
4
+  provider :github, ENV['GITHUB_OAUTH_KEY'], ENV['GITHUB_OAUTH_SECRET']
5
+end

+ 7 - 0
config/routes.rb

@@ -28,6 +28,12 @@ Huginn::Application.routes.draw do
28 28
 
29 29
   resources :user_credentials, :except => :show
30 30
 
31
+  resources :services, :only => [:index, :destroy] do
32
+    member do
33
+      post :toggle_availability
34
+    end
35
+  end
36
+
31 37
   get "/worker_status" => "worker_status#show"
32 38
 
33 39
   post "/users/:user_id/update_location/:secret" => "user_location_updates#create"
@@ -39,6 +45,7 @@ Huginn::Application.routes.draw do
39 45
 #  get "/delayed_job" => DelayedJobWeb, :anchor => false
40 46
 
41 47
   devise_for :users, :sign_out_via => [ :post, :delete ]
48
+  get '/auth/:provider/callback', to: 'services#callback'
42 49
 
43 50
   get "/about" => "home#about"
44 51
   root :to => "home#index"

+ 18 - 0
db/migrate/20140515211100_create_services.rb

@@ -0,0 +1,18 @@
1
+class CreateServices < ActiveRecord::Migration
2
+  def change
3
+    create_table :services do |t|
4
+      t.integer :user_id
5
+      t.string :provider
6
+      t.string :name
7
+      t.text :token
8
+      t.text :secret
9
+      t.text :refresh_token
10
+      t.datetime :expires_at
11
+      t.boolean :global, default: false
12
+      t.text :options
13
+      t.timestamps
14
+    end
15
+    add_index :services, :user_id
16
+    add_index :services, [:user_id, :global]
17
+  end
18
+end

+ 5 - 0
db/migrate/20140525150040_add_service_id_to_agents.rb

@@ -0,0 +1,5 @@
1
+class AddServiceIdToAgents < ActiveRecord::Migration
2
+  def change
3
+    add_column :agents, :service_id, :integer
4
+  end
5
+end

+ 39 - 0
db/migrate/20140525150140_migrate_agents_to_service_authentication.rb

@@ -0,0 +1,39 @@
1
+class MigrateAgentsToServiceAuthentication < ActiveRecord::Migration
2
+  def up
3
+    agents = Agent.where(type: ['Agents::TwitterUserAgent', 'Agents::TwitterStreamAgent', 'Agents::TwitterPublishAgent']).each do |agent|
4
+      service = agent.user.services.create!(
5
+        provider: 'twitter',
6
+        name: "Migrated '#{agent.name}'",
7
+        token: agent.twitter_oauth_token,
8
+        secret: agent.twitter_oauth_token_secret
9
+      )
10
+      agent.service_id = service.id
11
+      agent.save!
12
+    end
13
+    if agents.length > 0
14
+      puts <<-EOF.strip_heredoc
15
+
16
+        Your Twitter agents were successfully migrated. You need to update your .env file and add the following two lines:
17
+
18
+        TWITTER_OAUTH_KEY=#{agents.first.twitter_consumer_key}
19
+        TWITTER_OAUTH_SECRET=#{agents.first.twitter_consumer_secret}
20
+
21
+
22
+      EOF
23
+    end
24
+    if Agent.where(type: ['Agents::BasecampAgent']).count > 0
25
+      puts <<-EOF.strip_heredoc
26
+
27
+        Your Basecamp agents can not be migrated automatically. You need to manually register an application with 37signals and authenticate huginn to use it. 
28
+        Have a look at the <Wiki TBD> if you need help.
29
+
30
+
31
+      EOF
32
+    end
33
+  end
34
+
35
+  def down
36
+    raise ActiveRecord::IrreversibleMigration, "Cannot revert migration to OAuth services"
37
+  end
38
+end
39
+

+ 83 - 62
db/schema.rb

@@ -9,21 +9,24 @@
9 9
 # from scratch. The latter is a flawed and unsustainable approach (the more migrations
10 10
 # you'll amass, the slower it'll run and the greater likelihood for issues).
11 11
 #
12
-# It's strongly recommended to check this file into your version control system.
12
+# It's strongly recommended that you check this file into your version control system.
13 13
 
14
-ActiveRecord::Schema.define(:version => 20140408150825) do
14
+ActiveRecord::Schema.define(version: 20140525150140) do
15 15
 
16
-  create_table "agent_logs", :force => true do |t|
17
-    t.integer  "agent_id",                         :null => false
18
-    t.text     "message",                          :null => false
19
-    t.integer  "level",             :default => 3, :null => false
16
+  # These are extensions that must be enabled in order to support this database
17
+  enable_extension "plpgsql"
18
+
19
+  create_table "agent_logs", force: true do |t|
20
+    t.integer  "agent_id",                      null: false
21
+    t.text     "message",                       null: false
22
+    t.integer  "level",             default: 3, null: false
20 23
     t.integer  "inbound_event_id"
21 24
     t.integer  "outbound_event_id"
22
-    t.datetime "created_at",                       :null => false
23
-    t.datetime "updated_at",                       :null => false
25
+    t.datetime "created_at",                    null: false
26
+    t.datetime "updated_at",                    null: false
24 27
   end
25 28
 
26
-  create_table "agents", :force => true do |t|
29
+  create_table "agents", force: true do |t|
27 30
     t.integer  "user_id"
28 31
     t.text     "options"
29 32
     t.string   "type"
@@ -33,98 +36,116 @@ ActiveRecord::Schema.define(:version => 20140408150825) do
33 36
     t.datetime "last_check_at"
34 37
     t.datetime "last_receive_at"
35 38
     t.integer  "last_checked_event_id"
36
-    t.datetime "created_at",                                                     :null => false
37
-    t.datetime "updated_at",                                                     :null => false
38
-    t.text     "memory",                :limit => 2147483647
39
+    t.datetime "created_at",                            null: false
40
+    t.datetime "updated_at",                            null: false
41
+    t.text     "memory"
39 42
     t.datetime "last_web_request_at"
40
-    t.integer  "keep_events_for",                             :default => 0,     :null => false
43
+    t.integer  "keep_events_for",       default: 0,     null: false
41 44
     t.datetime "last_event_at"
42 45
     t.datetime "last_error_log_at"
43
-    t.boolean  "propagate_immediately",                       :default => false, :null => false
44
-    t.boolean  "disabled",                                    :default => false, :null => false
46
+    t.boolean  "propagate_immediately", default: false, null: false
47
+    t.boolean  "disabled",              default: false, null: false
48
+    t.integer  "service_id"
45 49
   end
46 50
 
47
-  add_index "agents", ["schedule"], :name => "index_agents_on_schedule"
48
-  add_index "agents", ["type"], :name => "index_agents_on_type"
49
-  add_index "agents", ["user_id", "created_at"], :name => "index_agents_on_user_id_and_created_at"
51
+  add_index "agents", ["schedule"], name: "index_agents_on_schedule", using: :btree
52
+  add_index "agents", ["type"], name: "index_agents_on_type", using: :btree
53
+  add_index "agents", ["user_id", "created_at"], name: "index_agents_on_user_id_and_created_at", using: :btree
50 54
 
51
-  create_table "delayed_jobs", :force => true do |t|
52
-    t.integer  "priority",                       :default => 0
53
-    t.integer  "attempts",                       :default => 0
54
-    t.text     "handler",    :limit => 16777215
55
+  create_table "delayed_jobs", force: true do |t|
56
+    t.integer  "priority",   default: 0
57
+    t.integer  "attempts",   default: 0
58
+    t.text     "handler"
55 59
     t.text     "last_error"
56 60
     t.datetime "run_at"
57 61
     t.datetime "locked_at"
58 62
     t.datetime "failed_at"
59 63
     t.string   "locked_by"
60 64
     t.string   "queue"
61
-    t.datetime "created_at",                                    :null => false
62
-    t.datetime "updated_at",                                    :null => false
65
+    t.datetime "created_at",             null: false
66
+    t.datetime "updated_at",             null: false
63 67
   end
64 68
 
65
-  add_index "delayed_jobs", ["priority", "run_at"], :name => "delayed_jobs_priority"
69
+  add_index "delayed_jobs", ["priority", "run_at"], name: "delayed_jobs_priority", using: :btree
66 70
 
67
-  create_table "events", :force => true do |t|
71
+  create_table "events", force: true do |t|
68 72
     t.integer  "user_id"
69 73
     t.integer  "agent_id"
70
-    t.decimal  "lat",                            :precision => 15, :scale => 10
71
-    t.decimal  "lng",                            :precision => 15, :scale => 10
72
-    t.text     "payload",    :limit => 16777215
73
-    t.datetime "created_at",                                                     :null => false
74
-    t.datetime "updated_at",                                                     :null => false
74
+    t.decimal  "lat",        precision: 15, scale: 10
75
+    t.decimal  "lng",        precision: 15, scale: 10
76
+    t.text     "payload"
77
+    t.datetime "created_at",                           null: false
78
+    t.datetime "updated_at",                           null: false
75 79
     t.datetime "expires_at"
76 80
   end
77 81
 
78
-  add_index "events", ["agent_id", "created_at"], :name => "index_events_on_agent_id_and_created_at"
79
-  add_index "events", ["expires_at"], :name => "index_events_on_expires_at"
80
-  add_index "events", ["user_id", "created_at"], :name => "index_events_on_user_id_and_created_at"
82
+  add_index "events", ["agent_id", "created_at"], name: "index_events_on_agent_id_and_created_at", using: :btree
83
+  add_index "events", ["expires_at"], name: "index_events_on_expires_at", using: :btree
84
+  add_index "events", ["user_id", "created_at"], name: "index_events_on_user_id_and_created_at", using: :btree
81 85
 
82
-  create_table "links", :force => true do |t|
86
+  create_table "links", force: true do |t|
83 87
     t.integer  "source_id"
84 88
     t.integer  "receiver_id"
85
-    t.datetime "created_at",                          :null => false
86
-    t.datetime "updated_at",                          :null => false
87
-    t.integer  "event_id_at_creation", :default => 0, :null => false
89
+    t.datetime "created_at",                       null: false
90
+    t.datetime "updated_at",                       null: false
91
+    t.integer  "event_id_at_creation", default: 0, null: false
92
+  end
93
+
94
+  add_index "links", ["receiver_id", "source_id"], name: "index_links_on_receiver_id_and_source_id", using: :btree
95
+  add_index "links", ["source_id", "receiver_id"], name: "index_links_on_source_id_and_receiver_id", using: :btree
96
+
97
+  create_table "services", force: true do |t|
98
+    t.integer  "user_id"
99
+    t.string   "provider"
100
+    t.string   "name"
101
+    t.text     "token"
102
+    t.text     "secret"
103
+    t.text     "refresh_token"
104
+    t.datetime "expires_at"
105
+    t.boolean  "global",        default: false
106
+    t.text     "options"
107
+    t.datetime "created_at"
108
+    t.datetime "updated_at"
88 109
   end
89 110
 
90
-  add_index "links", ["receiver_id", "source_id"], :name => "index_links_on_receiver_id_and_source_id"
91
-  add_index "links", ["source_id", "receiver_id"], :name => "index_links_on_source_id_and_receiver_id"
111
+  add_index "services", ["user_id", "global"], name: "index_accounts_on_user_id_and_global", using: :btree
112
+  add_index "services", ["user_id"], name: "index_accounts_on_user_id", using: :btree
92 113
 
93
-  create_table "user_credentials", :force => true do |t|
94
-    t.integer  "user_id",                              :null => false
95
-    t.string   "credential_name",                      :null => false
96
-    t.text     "credential_value",                     :null => false
97
-    t.datetime "created_at",                           :null => false
98
-    t.datetime "updated_at",                           :null => false
99
-    t.string   "mode",             :default => "text", :null => false
114
+  create_table "user_credentials", force: true do |t|
115
+    t.integer  "user_id",                           null: false
116
+    t.string   "credential_name",                   null: false
117
+    t.text     "credential_value",                  null: false
118
+    t.datetime "created_at",                        null: false
119
+    t.datetime "updated_at",                        null: false
120
+    t.string   "mode",             default: "text", null: false
100 121
   end
101 122
 
102
-  add_index "user_credentials", ["user_id", "credential_name"], :name => "index_user_credentials_on_user_id_and_credential_name", :unique => true
123
+  add_index "user_credentials", ["user_id", "credential_name"], name: "index_user_credentials_on_user_id_and_credential_name", unique: true, using: :btree
103 124
 
104
-  create_table "users", :force => true do |t|
105
-    t.string   "email",                  :default => "",    :null => false
106
-    t.string   "encrypted_password",     :default => "",    :null => false
125
+  create_table "users", force: true do |t|
126
+    t.string   "email",                  default: "",    null: false
127
+    t.string   "encrypted_password",     default: "",    null: false
107 128
     t.string   "reset_password_token"
108 129
     t.datetime "reset_password_sent_at"
109 130
     t.datetime "remember_created_at"
110
-    t.integer  "sign_in_count",          :default => 0
131
+    t.integer  "sign_in_count",          default: 0
111 132
     t.datetime "current_sign_in_at"
112 133
     t.datetime "last_sign_in_at"
113 134
     t.string   "current_sign_in_ip"
114 135
     t.string   "last_sign_in_ip"
115
-    t.datetime "created_at",                                :null => false
116
-    t.datetime "updated_at",                                :null => false
117
-    t.boolean  "admin",                  :default => false, :null => false
118
-    t.integer  "failed_attempts",        :default => 0
136
+    t.datetime "created_at",                             null: false
137
+    t.datetime "updated_at",                             null: false
138
+    t.boolean  "admin",                  default: false, null: false
139
+    t.integer  "failed_attempts",        default: 0
119 140
     t.string   "unlock_token"
120 141
     t.datetime "locked_at"
121
-    t.string   "username",                                  :null => false
122
-    t.string   "invitation_code",                           :null => false
142
+    t.string   "username",                               null: false
143
+    t.string   "invitation_code",                        null: false
123 144
   end
124 145
 
125
-  add_index "users", ["email"], :name => "index_users_on_email", :unique => true
126
-  add_index "users", ["reset_password_token"], :name => "index_users_on_reset_password_token", :unique => true
127
-  add_index "users", ["unlock_token"], :name => "index_users_on_unlock_token", :unique => true
128
-  add_index "users", ["username"], :name => "index_users_on_username", :unique => true
146
+  add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
147
+  add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
148
+  add_index "users", ["unlock_token"], name: "index_users_on_unlock_token", unique: true, using: :btree
149
+  add_index "users", ["username"], name: "index_users_on_username", unique: true, using: :btree
129 150
 
130 151
 end

+ 57 - 0
spec/controllers/services_controller_spec.rb

@@ -0,0 +1,57 @@
1
+require 'spec_helper'
2
+
3
+describe ServicesController do
4
+  before do
5
+    sign_in users(:bob)
6
+    OmniAuth.config.test_mode = true
7
+    request.env["omniauth.auth"] = JSON.parse(File.read(Rails.root.join('spec/data_fixtures/services/twitter.json')))
8
+  end
9
+
10
+  describe "GET index" do
11
+    it "only returns sevices of the current user" do
12
+      get :index
13
+      assigns(:services).all? {|i| i.user.should == users(:bob) }.should be_true
14
+    end
15
+  end
16
+
17
+  describe "POST toggle_availability" do
18
+    it "should work for service of the user" do
19
+      post :toggle_availability, :id => services(:generic).to_param
20
+      assigns(:service).should eq(services(:generic))
21
+      redirect_to(services_path)
22
+    end
23
+
24
+    it "should not work for a service of another user" do
25
+      lambda {
26
+        post :toggle_availability, :id => services(:global).to_param
27
+      }.should raise_error(ActiveRecord::RecordNotFound)
28
+    end
29
+  end
30
+
31
+  describe "DELETE destroy" do
32
+    it "destroys only services owned by the current user" do
33
+      expect {
34
+        delete :destroy, :id => services(:generic).to_param
35
+      }.to change(Service, :count).by(-1)
36
+
37
+      lambda {
38
+        delete :destroy, :id => services(:global).to_param
39
+      }.should raise_error(ActiveRecord::RecordNotFound)
40
+    end
41
+  end
42
+
43
+  describe "accepting a callback url" do
44
+    it "should update the users credentials" do
45
+      expect {
46
+        get :callback, provider: 'twitter'
47
+      }.to change { users(:bob).services.count }.by(1)
48
+    end
49
+
50
+    it "should not work with an unknown provider" do
51
+      request.env["omniauth.auth"]['provider'] = 'unknown'
52
+      expect {
53
+        get :callback, provider: 'unknown'
54
+      }.to change { users(:bob).services.count }.by(0)
55
+    end
56
+  end
57
+end

+ 43 - 0
spec/data_fixtures/services/37signals.json

@@ -0,0 +1,43 @@
1
+{
2
+  "provider": "37signals",
3
+  "uid": 12345,
4
+  "info": {
5
+    "email": "basecamp@none.de",
6
+    "first_name": "Dominik",
7
+    "last_name": "Sander",
8
+    "name": "Dominik Sander"
9
+  },
10
+  "credentials": {
11
+    "token": "abcde",
12
+    "refresh_token": "fghrefresh",
13
+    "expires_at": 1401554352,
14
+    "expires": true
15
+  },
16
+  "extra": {
17
+    "accounts": [
18
+      {
19
+        "product": "bcx",
20
+        "name": "Dominik Sander's Basecamp",
21
+        "id": 12345,
22
+        "href": "https://basecamp.com/12345/api/v1"
23
+      }
24
+    ],
25
+    "raw_info": {
26
+      "expires_at": "2014-05-31T16:39:12Z",
27
+      "identity": {
28
+        "first_name": "Dominik",
29
+        "last_name": "Sander",
30
+        "email_address": "basecamp@none.de",
31
+        "id": 12345
32
+      },
33
+      "accounts": [
34
+        {
35
+          "product": "bcx",
36
+          "name": "Dominik Sander's Basecamp",
37
+          "id": 12345,
38
+          "href": "https://basecamp.com/12345/api/v1"
39
+        }
40
+      ]
41
+    }
42
+  }
43
+}

+ 52 - 0
spec/data_fixtures/services/github.json

@@ -0,0 +1,52 @@
1
+{
2
+  "provider": "github",
3
+  "uid": "12345",
4
+  "info": {
5
+    "nickname": "dsander",
6
+    "email": null,
7
+    "name": "Dominik Sander",
8
+    "image": "https://avatars.githubusercontent.com/u/12345?",
9
+    "urls": {
10
+      "GitHub": "https://github.com/dsander",
11
+      "Blog": "http://www.dsander.de"
12
+    }
13
+  },
14
+  "credentials": {
15
+    "token": "agithubtoken",
16
+    "expires": false
17
+  },
18
+  "extra": {
19
+    "raw_info": {
20
+      "login": "dsander",
21
+      "id": 12345,
22
+      "avatar_url": "https://avatars.githubusercontent.com/u/12345?",
23
+      "gravatar_id": "fsdfsdf",
24
+      "url": "https://api.github.com/users/dsander",
25
+      "html_url": "https://github.com/dsander",
26
+      "followers_url": "https://api.github.com/users/dsander/followers",
27
+      "following_url": "https://api.github.com/users/dsander/following{/other_user}",
28
+      "gists_url": "https://api.github.com/users/dsander/gists{/gist_id}",
29
+      "starred_url": "https://api.github.com/users/dsander/starred{/owner}{/repo}",
30
+      "subscriptions_url": "https://api.github.com/users/dsander/subscriptions",
31
+      "organizations_url": "https://api.github.com/users/dsander/orgs",
32
+      "repos_url": "https://api.github.com/users/dsander/repos",
33
+      "events_url": "https://api.github.com/users/dsander/events{/privacy}",
34
+      "received_events_url": "https://api.github.com/users/dsander/received_events",
35
+      "type": "User",
36
+      "site_admin": false,
37
+      "name": "Dominik Sander",
38
+      "company": null,
39
+      "blog": "http://www.url.de",
40
+      "location": null,
41
+      "email": null,
42
+      "hireable": false,
43
+      "bio": null,
44
+      "public_repos": 29,
45
+      "public_gists": 2,
46
+      "followers": 21,
47
+      "following": 9,
48
+      "created_at": "2008-08-17T18:17:50Z",
49
+      "updated_at": "2014-05-19T09:30:08Z"
50
+    }
51
+  }
52
+}

+ 66 - 0
spec/data_fixtures/services/twitter.json

@@ -0,0 +1,66 @@
1
+{
2
+  "provider": "twitter",
3
+  "uid": "123456",
4
+  "info": {
5
+    "nickname": "johnqpublic",
6
+    "name": "John Q Public",
7
+    "location": "Anytown, USA",
8
+    "image": "http://si0.twimg.com/sticky/default_profile_images/default_profile_2_normal.png",
9
+    "description": "a very normal guy.",
10
+    "urls": {
11
+      "Website": null,
12
+      "Twitter": "https://twitter.com/johnqpublic"
13
+    }
14
+  },
15
+  "credentials": {
16
+    "token": "a1b2c3d4...",
17
+    "secret": "abcdef1234"
18
+  },
19
+  "extra": {
20
+    "access_token": "",
21
+    "raw_info": {
22
+      "name": "John Q Public",
23
+      "listed_count": 0,
24
+      "profile_sidebar_border_color": "181A1E",
25
+      "url": null,
26
+      "lang": "en",
27
+      "statuses_count": 129,
28
+      "profile_image_url": "http://si0.twimg.com/sticky/default_profile_images/default_profile_2_normal.png",
29
+      "profile_background_image_url_https": "https://twimg0-a.akamaihd.net/profile_background_images/229171796/pattern_036.gif",
30
+      "location": "Anytown, USA",
31
+      "time_zone": "Chicago",
32
+      "follow_request_sent": false,
33
+      "id": 123456,
34
+      "profile_background_tile": true,
35
+      "profile_sidebar_fill_color": "666666",
36
+      "followers_count": 1,
37
+      "default_profile_image": false,
38
+      "screen_name": "",
39
+      "following": false,
40
+      "utc_offset": -3600,
41
+      "verified": false,
42
+      "favourites_count": 0,
43
+      "profile_background_color": "1A1B1F",
44
+      "is_translator": false,
45
+      "friends_count": 1,
46
+      "notifications": false,
47
+      "geo_enabled": true,
48
+      "profile_background_image_url": "http://twimg0-a.akamaihd.net/profile_background_images/229171796/pattern_036.gif",
49
+      "protected": false,
50
+      "description": "a very normal guy.",
51
+      "profile_link_color": "2FC2EF",
52
+      "created_at": "Thu Jul 4 00:00:00 +0000 2013",
53
+      "id_str": "123456",
54
+      "profile_image_url_https": "https://si0.twimg.com/sticky/default_profile_images/default_profile_2_normal.png",
55
+      "default_profile": false,
56
+      "profile_use_background_image": false,
57
+      "entities": {
58
+        "description": {
59
+          "urls": []
60
+        }
61
+      },
62
+      "profile_text_color": "666666",
63
+      "contributors_enabled": false
64
+    }
65
+  }
66
+}

+ 17 - 0
spec/fixtures/services.yml

@@ -0,0 +1,17 @@
1
+generic:
2
+  token: 1234token
3
+  secret: 56789secret
4
+  refresh_token: refresh12345
5
+  provider: testprovider
6
+  name: test
7
+  expires_at: <%= Time.parse("2015-01-01 00:00:00") %>
8
+  options: <%= { user_id: 12345 }.to_yaml.inspect %>
9
+  user: bob
10
+global:
11
+  token: 1234token
12
+  provider: testprovider
13
+  name: test
14
+  expires_at: <%= Time.parse("2015-01-01 00:00:00") %>
15
+  options: <%= { user_id: 12345 }.to_yaml.inspect %>
16
+  user: jane
17
+  global: true

+ 6 - 22
spec/models/agents/basecamp_agent_spec.rb

@@ -1,17 +1,16 @@
1 1
 require 'spec_helper'
2
+require 'models/concerns/oauthable'
2 3
 
3 4
 describe Agents::BasecampAgent do
5
+  it_behaves_like Oauthable
6
+
4 7
   before(:each) do
5 8
     stub_request(:get, /json$/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/basecamp.json")), :status => 200, :headers => {"Content-Type" => "text/json"})
6 9
     stub_request(:get, /Z$/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/basecamp.json")), :status => 200, :headers => {"Content-Type" => "text/json"})
7
-    @valid_params = {
8
-                      :username   => "user",
9
-                      :password   => "pass",
10
-                      :user_id    => 12345,
11
-                      :project_id => 6789,
12
-                    }
10
+    @valid_params = { :project_id => 6789 }
13 11
 
14 12
     @checker = Agents::BasecampAgent.new(:name => "somename", :options => @valid_params)
13
+    @checker.service = services(:generic)
15 14
     @checker.user = users(:jane)
16 15
     @checker.save!
17 16
   end
@@ -21,21 +20,6 @@ describe Agents::BasecampAgent do
21 20
       @checker.should be_valid
22 21
     end
23 22
 
24
-    it "should require the basecamp username" do
25
-      @checker.options['username'] = nil
26
-      @checker.should_not be_valid
27
-    end
28
-
29
-    it "should require the basecamp password" do
30
-      @checker.options['password'] = nil
31
-      @checker.should_not be_valid
32
-    end
33
-
34
-    it "should require the basecamp user_id" do
35
-      @checker.options['user_id'] = nil
36
-      @checker.should_not be_valid
37
-    end
38
-
39 23
     it "should require the basecamp project_id" do
40 24
       @checker.options['project_id'] = nil
41 25
       @checker.should_not be_valid
@@ -45,7 +29,7 @@ describe Agents::BasecampAgent do
45 29
 
46 30
   describe "helpers" do
47 31
     it "should generate a correct request options hash" do
48
-      @checker.send(:request_options).should == {:basic_auth=>{:username=>"user", :password=>"pass"}, :headers => {"User-Agent" => "Huginn (https://github.com/cantino/huginn)"}}
32
+      @checker.send(:request_options).should == {:headers => {"User-Agent" => "Huginn (https://github.com/cantino/huginn)", "Authorization" => 'Bearer "1234token"'}}
49 33
     end
50 34
 
51 35
     it "should generate the currect request url" do

+ 1 - 0
spec/models/agents/twitter_publish_agent_spec.rb

@@ -13,6 +13,7 @@ describe Agents::TwitterPublishAgent do
13 13
     }
14 14
 
15 15
     @checker = Agents::TwitterPublishAgent.new(:name => "HuginnBot", :options => @opts)
16
+    @checker.service = services(:generic)
16 17
     @checker.user = users(:bob)
17 18
     @checker.save!
18 19
 

+ 1 - 0
spec/models/agents/twitter_stream_agent_spec.rb

@@ -13,6 +13,7 @@ describe Agents::TwitterStreamAgent do
13 13
     }
14 14
 
15 15
     @agent = Agents::TwitterStreamAgent.new(:name => "HuginnBot", :options => @opts)
16
+    @agent.service = services(:generic)
16 17
     @agent.user = users(:bob)
17 18
     @agent.save!
18 19
   end

+ 1 - 0
spec/models/agents/twitter_user_agent_spec.rb

@@ -15,6 +15,7 @@ describe Agents::TwitterUserAgent do
15 15
     }
16 16
 
17 17
     @checker = Agents::TwitterUserAgent.new(:name => "tectonic", :options => @opts)
18
+    @checker.service = services(:generic)
18 19
     @checker.user = users(:bob)
19 20
     @checker.save!
20 21
   end

+ 29 - 0
spec/models/concerns/oauthable.rb

@@ -0,0 +1,29 @@
1
+require 'spec_helper'
2
+
3
+module Agents
4
+  class OauthableTestAgent < Agent
5
+    include Oauthable
6
+  end
7
+end
8
+
9
+shared_examples_for Oauthable do
10
+  before(:each) do
11
+    @agent = described_class.new(:name => "somename")
12
+    @agent.user = users(:jane)
13
+  end
14
+
15
+  it "should be oauthable" do
16
+    @agent.oauthable?.should == true
17
+  end
18
+
19
+  describe "valid_services" do
20
+    it "should return all available services without specifying valid_oauth_providers" do
21
+      @agent = Agents::OauthableTestAgent.new
22
+      @agent.valid_services(users(:bob)).collect(&:id).sort.should == [services(:generic), services(:global)].collect(&:id).sort
23
+    end
24
+
25
+    it "should filter the services based on the agent defaults" do
26
+      @agent.valid_services(users(:bob)).to_a.should == Service.where(provider: @agent.valid_oauth_providers)
27
+    end
28
+  end
29
+end

+ 100 - 0
spec/models/service_spec.rb

@@ -0,0 +1,100 @@
1
+require 'spec_helper'
2
+
3
+describe Service do
4
+  before(:each) do
5
+    @user = users(:bob)
6
+  end
7
+
8
+  it "should toggle the global flag" do
9
+    @service = services(:generic)
10
+    @service.global.should == false
11
+    @service.toggle_availability!
12
+    @service.global.should == true
13
+    @service.toggle_availability!
14
+    @service.global.should == false
15
+  end
16
+
17
+  describe "preparing for a request" do
18
+    before(:each) do
19
+      @service = services(:generic)
20
+    end
21
+
22
+    it "should not update the token if the token never expires" do
23
+      @service.expires_at = nil
24
+      @service.prepare_request.should == nil
25
+    end
26
+
27
+    it "should not update the token if the token is still valid" do
28
+      @service.expires_at = Time.now + 1.hour
29
+      @service.prepare_request.should == nil
30
+    end
31
+
32
+    it "should call refresh_token! if the token expired" do
33
+      stub(@service).refresh_token! { @service }
34
+      @service.expires_at = Time.now - 1.hour
35
+      @service.prepare_request.should == @service
36
+    end
37
+  end
38
+
39
+  describe "updating the access token" do
40
+    before(:each) do
41
+      @service = services(:generic)
42
+    end
43
+
44
+    it "should return the correct endpoint" do
45
+      @service.provider = '37signals'
46
+      @service.send(:endpoint).to_s.should == "https://launchpad.37signals.com/authorization/token"
47
+    end
48
+
49
+    it "should update the token" do
50
+      stub_request(:post, "https://launchpad.37signals.com/authorization/token?client_id=TESTKEY&client_secret=TESTSECRET&refresh_token=refreshtokentest&type=refresh").
51
+        to_return(:status => 200, :body => '{"expires_in":1209600,"access_token": "NEWTOKEN"}', :headers => {})
52
+      @service.provider = '37signals'
53
+      ENV['37SIGNALS_OAUTH_KEY'] = 'TESTKEY'
54
+      ENV['37SIGNALS_OAUTH_SECRET'] = 'TESTSECRET'
55
+      @service.refresh_token = 'refreshtokentest'
56
+      @service.refresh_token!
57
+      @service.token.should == 'NEWTOKEN'
58
+    end
59
+  end
60
+
61
+  describe "creating services via omniauth" do
62
+    it "should work with twitter services" do
63
+      twitter = JSON.parse(File.read(Rails.root.join('spec/data_fixtures/services/twitter.json')))
64
+      expect {
65
+        service = @user.services.initialize_or_update_via_omniauth(twitter)
66
+        service.save!
67
+      }.to change { @user.services.count }.by(1)
68
+      service = @user.services.first
69
+      service.name.should == 'johnqpublic'
70
+      service.provider.should == 'twitter'
71
+      service.token.should == 'a1b2c3d4...'
72
+      service.secret.should == 'abcdef1234'
73
+    end
74
+    it "should work with 37signals services" do
75
+      signals = JSON.parse(File.read(Rails.root.join('spec/data_fixtures/services/37signals.json')))
76
+      expect {
77
+        service = @user.services.initialize_or_update_via_omniauth(signals)
78
+        service.save!
79
+      }.to change { @user.services.count }.by(1)
80
+      service = @user.services.first
81
+      service.provider.should == '37signals'
82
+      service.name.should == 'Dominik Sander'
83
+      service.token.should == 'abcde'
84
+      service.refresh_token.should == 'fghrefresh'
85
+      service.options[:user_id].should == 12345
86
+      service.expires_at = Time.at(1401554352)
87
+    end
88
+    it "should work with github services" do
89
+      signals = JSON.parse(File.read(Rails.root.join('spec/data_fixtures/services/github.json')))
90
+      expect {
91
+        service = @user.services.initialize_or_update_via_omniauth(signals)
92
+        service.save!
93
+      }.to change { @user.services.count }.by(1)
94
+      service = @user.services.first
95
+      service.provider.should == 'github'
96
+      service.name.should == 'dsander'
97
+      service.token.should == 'agithubtoken'
98
+    end
99
+  end
100
+end

+ 2 - 0
spec/spec_helper.rb

@@ -21,6 +21,8 @@ WebMock.disable_net_connect!
21 21
 # in spec/support/ and its subdirectories.
22 22
 Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f}
23 23
 
24
+ActiveRecord::Migration.maintain_test_schema!
25
+
24 26
 RSpec.configure do |config|
25 27
   config.mock_with :rr
26 28